using System;
using System.Collections;
using System.Net;

namespace SMS.SMPPClient.Util
{
    /// <summary>
    /// Tag, length, value table for SMPP PDUs.
    /// </summary>
    public class TLVTable
    {
        private Hashtable tlvTable;
        /// <summary>
        /// Creates a TLV table.
        /// </summary>
        public TLVTable()
        {
            tlvTable = new Hashtable();

            tlvTable = Hashtable.Synchronized(tlvTable);
        }

        /// <summary>
        /// Converts the TLV byte array data into the Hashtable.  This will check if
        /// the passed in data is null or is an empty array so you don't need to
        /// check it before calling this method.  This is equivalent to
        /// TranslateTLVDataIntoTable( tlvData, 0 ).
        /// </summary>
        /// <param name="tlvData">The bytes of TLVs.</param>
        public void TranslateTLVDataIntoTable(byte[] tlvData)
        {
            TranslateTLVDataIntoTable(tlvData, 0);
        }

        /// <summary>
        /// Converts the TLV byte array data into the Hashtable.  This will check if
        /// the passed in data is null or is an empty array so you don't need to check
        /// it before calling this method.
        /// </summary>
        /// <param name="tlvData">The bytes of TLVs.</param>
        /// <param name="index">The index of the byte array to start at.  This is here
        /// because in some instances you may not want to start at the
        /// beginning.</param>
        public void TranslateTLVDataIntoTable(byte[] tlvData, Int32 index)
        {
            if (tlvData == null || tlvData.Length <= 0)
            {
                return;
            }

            //go through and decode the TLVs

            while (index < tlvData.Length)
            {
                InjectTLV(tlvData, ref index);
            }
        }

        /// <summary>
        /// Using the given tlvData byte array and the given starting index, inserts
        /// the tag and value (as a byte array) into the hashtable.  Note that the
        /// tlvData needs to be in the SMPP v3.4 format (tag, length, value).
        ///
        /// Note also that this will advance the index by the TLV data length so that
        /// it may be used for consecutive reads from the same array.
        /// </summary>
        /// <param name="tlvData">The TLV data as a byte array.</param>
        /// <param name="index">The index of the array to start reading from.</param>
        private void InjectTLV(byte[] tlvData,
                                ref Int32 index)
        {
            UInt16 tag = BitConverter.ToUInt16(tlvData, index);
            index += 2;
            UInt16 length = BitConverter.ToUInt16(tlvData, index);
            index += 2;

            //decode the value
            ArrayList data = new ArrayList(length);

            int total = index + length;
            for (int k = index; (k < index + length) &&
                    k < tlvData.Length; k++)
            {
                data.Add(tlvData[k]);
            }

            data.TrimToSize();

            //add the values to the hashtable
            tlvTable.Add(tag, data.ToArray(typeof(byte)));

            //set it up for the next run
            index += length;
        }

        /// <summary>
        /// Gets the optional parameter bytes associated with the given tag.
        /// </summary>
        /// <param name="tag">The tag in TLV.</param>
        /// <returns>The optional parameter bytes, null if not found.</returns>
        public byte[] GetOptionalParamBytes(UInt16 tag)
        {
            object val = tlvTable[tag];

            if (val == null)
            {
                return null;
            }
            else
            {
                return (byte[])val;
            }
        }

        /// <summary>
        /// Gets the optional parameter string associated with the given tag.
        /// </summary>
        /// <param name="tag">The tag in TLV.</param>
        /// <returns>The optional parameter string, the empty string if not found.
        /// </returns>
        public string GetOptionalParamString(UInt16 tag)
        {
            byte[] val = GetOptionalParamBytes(tag);

            if (val == null)
            {
                return "";
            }
            return System.Text.Encoding.ASCII.GetString(val);
        }

        /// <summary>
        /// Sets the given TLV (as a byte array) into the table.  This will not take
        /// care of big-endian/little-endian issues.  This ignores null values.
        /// </summary>
        /// <param name="tag">The tag for this TLV.</param>
        /// <param name="val">The value of this TLV.</param>
        public void SetOptionalParamBytes(UInt16 tag, byte[] val)
        {
            if (val != null)
            {
                if (val.Length > UInt16.MaxValue)
                {
                    throw new Exception("Optional parameter value for "
                                         + tag + " is too large.");
                }

                ArrayList data = new ArrayList();
                //tag
                data.AddRange(BitConverter.GetBytes(tag));
                //length
                data.AddRange(BitConverter.GetBytes(
                                     (UInt16)val.Length));
                data.AddRange(val);

                int index = 0;
                InjectTLV((byte[])data.ToArray(typeof(byte)),
                             ref index);
            }
        }

        /// <summary>
        /// Sets the given TLV (as a string) into the table.  This ignores
        /// null values.
        /// </summary>
        /// <param name="tag">The tag for this TLV.</param>
        /// <param name="val">The value of this TLV.</param>
        public void SetOptionalParamString(UInt16 tag, string val)
        {
            if (val != null)
                SetOptionalParamBytes(tag,
                                         System.Text.Encoding.ASCII.GetBytes(val));
        }

        /// <summary>
        /// Allows the updating of TLV values.
        /// </summary>
        /// <param name="tag">The tag for this TLV.</param>
        /// <param name="val">The value of this TLV.</param>
        public void UpdateOptionalParamBytes(UInt16 tag, byte[] val)
        {
            tlvTable[tag] = val;
        }

        /// <summary>
        /// Allows the updating of TLV values.
        /// </summary>
        /// <param name="tag">The tag for this TLV.</param>
        /// <param name="val">The value of this TLV.</param>
        public void UpdateOptionalParamString(UInt16 tag, string val)
        {
            UpdateOptionalParamBytes(tag,
                                        System.Text.Encoding.ASCII.GetBytes(val));
        }

        /// <summary>
        /// Iterates through the hashtable, gathering the tag, length, and
        /// value as it goes.  For each entry, it encodes the TLV into a byte
        /// array.  The tag is converted to network order before being encoded.
        /// </summary>
        /// <returns>An ArrayList consisting of byte array entries, each of which
        /// is a TLV.  Returns an empty ArrayList if the TLV table is empty.</returns>
        public ArrayList GenerateByteEncodedTLV()
        {
            if (tlvTable == null || tlvTable.Count <= 0)
            {
                return new ArrayList(0);
            }

            ArrayList tlvs = new ArrayList();

            IDictionaryEnumerator iterator = tlvTable.GetEnumerator();

            ArrayList elem = new ArrayList();
            while (iterator.MoveNext())
            {
                elem.Clear();
                //tag-2 bytes
                elem.AddRange(BitConverter.GetBytes(
                                     UnsignedNumConverter.SwapByteOrdering((UInt16)iterator.Key)));
                //length-2 bytes
                elem.AddRange(BitConverter.GetBytes(
                                     IPAddress.HostToNetworkOrder(
                                         ((byte[])iterator.Value).Length)));
                //value
                elem.AddRange((byte[])iterator.Value);
                elem.TrimToSize();

                //copy it over to a byte array
                tlvs.Add(elem.ToArray(typeof(byte)));
            }

            tlvs.TrimToSize();

            return tlvs;
        }
    }
}
